/**
 * @Description: 公共工具类
 * @Author: Zander
 * @Date: 2022/11/29 11:15
 * @LastEditors: Zander
 * @LastEditTime: 2022/11/29 11:15
 */
import { isArray, isEmpty, isObject, isString } from './verify';
import { number } from './validate';

class CommonUtils {
  // 去除空格 pos both(左右)|left|right|all 默认both
  trim(str, pos = 'both') {
    str = String(str)
    if (pos === 'both') {
      return str.replace(/^\s+|\s+$/g, '')
    }
    if (pos === 'left') {
      return str.replace(/^\s*/, '')
    }
    if (pos === 'right') {
      return str.replace(/(\s*$)/g, '')
    }
    if (pos === 'all') {
      return str.replace(/\s+/g, '')
    }
    return str
  }

  // 添加单位，如果有rpx，upx，%，px等单位结尾或者值为auto，直接返回，否则加上rpx单位结尾
  addUnit(value = 'auto', unit = 'rpx') {
    value = String(value);
    // 用内置验证规则中的number判断是否为数值
    return number(value) ? `${value}${unit}` : value;
  }

  // 样式转换 对象转字符串，或者字符串转对象
  // eslint-disable-next-line complexity
  addStyle(customStyle, target = 'object') {
    const that = this;
    // 字符串转字符串，对象转对象情形，直接返回
    if (isEmpty(customStyle) || (isObject(customStyle) && target === 'object') || (isString(customStyle) && target === 'string')) {
      return customStyle;
    }
    // 字符串转对象
    if (target === 'object') {
      // 去除字符串样式中的两端空格(中间的空格不能去掉，比如padding: 20px 0如果去掉了就错了)，空格是无用的
      customStyle = that.trim(customStyle)
      // 根据";"将字符串转为数组形式
      const styleArray = customStyle.split(';')
      const style = {}
      // 历遍数组，拼接成对象
      for (let i = 0; i < styleArray.length; i++) {
        // 'font-size:20px;color:red;'，如此最后字符串有";"的话，会导致styleArray最后一个元素为空字符串，这里需要过滤
        if (styleArray[i]) {
          const item = styleArray[i].split(':')
          style[that.trim(item[0])] = that.trim(item[1])
        }
      }
      return style
    }
    // 这里为对象转字符串形式
    let string = ''
    for (const i in customStyle) {
      // 驼峰转为中划线的形式，否则css内联样式，无法识别驼峰样式属性名
      if (Object.prototype.hasOwnProperty.call(customStyle, i)) {
        const key = i.replace(/([A-Z])/g, '-$1').toLowerCase()
        string += `${key}:${customStyle[i]};`
      }
    }
    // 去除两端空格
    return that.trim(string)
  }

  /**
   * @description 用于获取用户传递值的px值  如果用户传递了"xxpx"或者"xxrpx"，取出其数值部分，如果是"xxxrpx"还需要用过uni.upx2px进行转换
   * @param {number|string} value 用户传递值的px值
   * @param {boolean} unit
   * @returns {number|string}
   */
  getPx(value, unit = false) {
    if (number(value)) {
      return unit ? `${value}px` : Number(value)
    }
    // 如果带有rpx，先取出其数值部分，再转为px值
    if (/(rpx|upx)$/.test(value)) {
      return unit ? `${uni.upx2px(parseInt(value))}px` : Number(uni.upx2px(parseInt(value)))
    }
    return unit ? `${parseInt(value)}px` : parseInt(value)
  }

  /**
   * @description 运行期判断平台
   * @returns {string} 返回所在平台(小写)
   * @link 运行期判断平台 https://uniapp.dcloud.io/frame?id=判断平台
   */
  os() {
    return uni.getSystemInfoSync().platform.toLowerCase()
  }

  /**
   * @description 获取系统信息同步接口
   * @link 获取系统信息同步接口 https://uniapp.dcloud.io/api/system/info?id=getsysteminfosync
   */
  sys() {
    return uni.getSystemInfoSync()
  }

  getLocation() {
    return new Promise((resolve, reject) => {
      uni.getLocation({
        type: 'gcj02',
        isHighAccuracy: true,
        success: res => {
          resolve(res)
        },
        fail: err => {
          reject(err)
        }
      })
    })
  }

  /**
   * @description 进行延时，以达到可以简写代码的目的 比如: await uni.$u.sleep(20)将会阻塞20ms
   * @param {number} value 堵塞时间 单位ms 毫秒
   * @returns {Promise} 返回promise
   */
  sleep(value = 30) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve()
      }, value)
    })
  }

  /**
   * 格式化时间
   * @param dateTime
   * @param fmt
   * @returns {null|*}
   * @constructor
   */
  dateFormat(dateTime, fmt = 'yyyy年MM月dd日') {
    // 格式化时间
    // yyyy:MM:dd|yyyy:MM|yyyy年MM月dd日|yyyy年MM月dd日 hh时mm分等,可自定义组合
    // 如果为null,则格式化当前时间
    if (isEmpty(dateTime)) {
      dateTime = new Date();
    }
    // 如果dateTime长度为10或者13，则为秒和毫秒的时间戳，如果超过13位，则为其他的时间格式
    if (dateTime.toString().length === 10 && new RegExp('^[1-9]\\d*|0$').test(dateTime)) {
      dateTime *= 1000;
    }
    const date = new Date(dateTime);
    let ret;
    const opt = {
      'y+': date.getFullYear().toString(), // 年
      'M+': (date.getMonth() + 1).toString(), // 月
      'd+': date.getDate().toString(), // 日
      'H+': date.getHours().toString(), // 时
      'm+': date.getMinutes().toString(), // 分
      's+': date.getSeconds().toString() // 秒
      // 有其他格式化字符需求可以继续添加，必须转化成字符串
    };
    for (const k in opt) {
      ret = new RegExp('(' + k + ')').exec(fmt);
      if (ret) {
        fmt = fmt.replace(
          ret[1],
          ret[1].length === 1 ? opt[k] : opt[k].padStart(ret[1].length, '0')
        );
      }
    }
    return fmt;
  }

  // eslint-disable-next-line
  queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') {
    const prefix = isPrefix ? '?' : '';
    const _result = [];
    if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) === -1) arrayFormat = 'brackets'
    for (const key in data) {
      const value = data[key]
      // 去掉为空的参数
      if (['', undefined, null].indexOf(value) >= 0) {
        continue
      }
      // 如果值为数组，另行处理
      if (value.constructor === Array) {
        // e.g. {ids: [1, 2, 3]}
        switch (arrayFormat) {
          case 'indices':
            // 结果: ids[0]=1&ids[1]=2&ids[2]=3
            for (let i = 0; i < value.length; i++) {
              _result.push(`${key}[${i}]=${value[i]}`)
            }
            break
          case 'brackets':
            // 结果: ids[]=1&ids[]=2&ids[]=3
            value.forEach((_value) => {
              _result.push(`${key}[]=${_value}`)
            })
            break
          case 'repeat':
            // 结果: ids=1&ids=2&ids=3
            value.forEach((_value) => {
              _result.push(`${key}=${_value}`)
            })
            break
          case 'comma':
            // 结果: ids=1,2,3
            // eslint-disable-next-line
            let commaStr = ''
            value.forEach((_value) => {
              commaStr += (commaStr ? ',' : '') + _value
            })
            _result.push(`${key}=${commaStr}`)
            break
          default:
            value.forEach((_value) => {
              _result.push(`${key}[]=${_value}`)
            })
        }
      } else {
        _result.push(`${key}=${value}`)
      }
    }
    return _result.length ? prefix + _result.join('&') : ''
  }

  /**
   * Iterate over an Array or an Object invoking a function for each item.
   *
   * If `obj` is an Array callback will be called passing
   * the value, index, and complete array for each item.
   *
   * If 'obj' is an Object callback will be called passing
   * the value, key, and complete object for each property.
   *
   * @param {Object|Array} obj The object to iterate
   * @param {Function} fn The callback to invoke for each item
   */
  forEach(obj, fn) {
    // Don't bother if no value provided
    if (obj === null || typeof obj === 'undefined') {
      return
    }

    // Force an array if not already something iterable
    if (typeof obj !== 'object') {
      /* eslint no-param-reassign:0 */
      obj = [obj]
    }

    if (isArray(obj)) {
      // Iterate over array values
      for (let i = 0, l = obj.length; i < l; i++) {
        // eslint-disable-next-line
        fn.call(null, obj[i], i, obj)
      }
    } else {
      // Iterate over object keys
      for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
          // eslint-disable-next-line
          fn.call(null, obj[key], key, obj)
        }
      }
    }
  }

  // 合并对象
  merge(/* obj1, obj2, obj3, ... */) {
    const result = {}, that = this;
    function assignValue(val, key) {
      if (typeof result[key] === 'object' && typeof val === 'object') {
        result[key] = that.merge(result[key], val)
      } else if (typeof val === 'object') {
        result[key] = that.merge({}, val)
      } else {
        result[key] = val
      }
    }
    for (let i = 0, l = arguments.length; i < l; i++) {
      that.forEach(arguments[i], assignValue)
    }
    return result
  }

  /**
   * 深度克隆
   * @param {object} obj 需要深度克隆的对象
   * @returns {*[]|*} 克隆后的对象或者原值（不是对象）
   */
  deepClone(obj) {
    // 对常见的“非”值，直接返回原来值
    if ([null, undefined, NaN, false].includes(obj)) return obj;
    if (typeof obj !== 'object' && typeof obj !== 'function') {
      // 原始类型直接返回
      return obj
    }
    const map = isArray(obj) ? [] : {}
    for (const i in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, i)) {
        map[i] = typeof obj[i] === 'object' ? this.deepClone(obj[i]) : obj[i]
      }
    }
    return map
  }

  /**
   * JS对象深度合并
   * @param {object} target 需要拷贝的对象
   * @param {object} source 拷贝的来源对象
   * @returns {*[]|*|boolean} 深度合并后的对象或者false（入参有不是对象）
   */
  deepMerge(target = {}, source = {}) {
    target = this.deepClone(target)
    if (typeof target !== 'object' || typeof source !== 'object') return false
    for (const prop in source) {
      if (!Object.prototype.hasOwnProperty.call(source, prop)) continue
      if (prop in target) {
        if (typeof target[prop] !== 'object') {
          target[prop] = source[prop]
        } else if (typeof source[prop] !== 'object') {
          target[prop] = source[prop]
        } else if (target[prop].concat && source[prop].concat) {
          target[prop] = target[prop].concat(source[prop])
        } else {
          target[prop] = this.deepMerge(target[prop], source[prop])
        }
      } else {
        target[prop] = source[prop]
      }
    }
    return target
  }

  error(msg) {
    // 开发环境才提示，生产环境不会提示
    if (process.env.NODE_ENV === 'development') {
      console.error(`提示：${msg}`)
    }
  }

  /**
   * 显示消息提示框
   * @param {String} title 提示的内容，长度与 icon 取值有关。
   * @param {String} icon 提示的图标。
   * @param {Number} duration 提示的延迟时间，单位毫秒，默认：2000
   */
  toast(title, icon = 'none', duration = 2000) {
    uni.showToast({
      title: String(title),
      icon,
      duration
    })
  }

  /**
   * 获取当前页面路径
   * @returns {string}
   */
  getPageUrl() {
    const pages = getCurrentPages()
    // 某些特殊情况下(比如页面进行redirectTo时的一些时机)，pages可能为空数组
    return `/${pages[pages.length - 1]?.route ?? ''}`
  }

  /**
   * 获取当前路由栈实例数组
   * @returns {Array<tinyapp.IPageInstance<any>> | Array<WechatMiniprogram.Page.Instance<WechatMiniprogram.IAnyObject, WechatMiniprogram.IAnyObject>>}
   */
  getPages() {
    return getCurrentPages();
  }

  /**
   * @description 获取父组件的参数，因为支付宝小程序不支持provide/inject的写法
   * this.$parent在非H5中，可以准确获取到父组件，但是在H5中，需要多次this.$parent.$parent.xxx
   * 这里默认值等于undefined有它的含义，因为最顶层元素(组件)的$parent就是undefined，意味着不传name
   * 值(默认为undefined)，就是查找最顶层的$parent
   * @param {string|undefined} name 父组件的参数名
   * @returns {any}
   */
  $parent(name = undefined) {
    let parent = this.$parent
    // 通过while历遍，这里主要是为了H5需要多层解析的问题
    while (parent) {
      // 父组件
      if (parent.$options && parent.$options.name !== name) {
        // 如果组件的name不相等，继续上一级寻找
        parent = parent.$parent
      } else {
        return parent
      }
    }
    return false
  }

  /**
   * @description 在u-form的子组件内容发生变化，或者失去焦点时，尝试通知u-form执行校验方法
   * @param {*} instance
   * @param {*} event
   */
  formValidate(instance, event) {
    const formItem = this.$parent.call(instance, 'u-form-item')
    const form = this.$parent.call(instance, 'u-form')
    // 如果发生变化的input或者textarea等，其父组件中有u-form-item或者u-form等，就执行form的validate方法
    // 同时将form-item的pros传递给form，让其进行精确对象验证
    if (formItem && form) {
      form.validateField(formItem.prop, () => {}, event)
    }
  }

  /**
   * 获取某个对象下的属性，用于通过类似'a.b.c'的形式去获取一个对象的的属性的形式
   * @param {object} obj 对象
   * @param {string} key 需要获取的属性字段
   * @returns {string|*|null|{}}
   */
  getProperty(obj, key) {
    if (!obj) {
      return null
    }
    if (typeof key !== 'string' || key === '') {
      return ''
    }
    if (key.indexOf('.') !== -1) {
      const keys = key.split('.')
      let firstObj = obj[keys[0]] || {}

      for (let i = 1; i < keys.length; i++) {
        if (firstObj) {
          firstObj = firstObj[keys[i]]
        }
      }
      return firstObj
    }
    return obj[key]
  }

  /**
   * 设置对象的属性值，如果'a.b.c'的形式进行设置
   * @param {object} obj 对象
   * @param {string} key 需要设置的属性
   * @param {string} value 设置的值
   */
  setProperty(obj, key, value) {
    if (!obj) {
      return
    }
    // 递归赋值
    const inFn = function(_obj, keys, v) {
      // 最后一个属性key
      if (keys.length === 1) {
        _obj[keys[0]] = v
        return
      }
      // 0~length-1个key
      while (keys.length > 1) {
        const k = keys[0]
        if (!_obj[k] || (typeof _obj[k] !== 'object')) {
          _obj[k] = {}
        }
        // eslint-disable-next-line
        const key = keys.shift()
        // 自调用判断是否存在属性，不存在则自动创建对象
        inFn(_obj[k], keys, v)
      }
    }

    if (typeof key !== 'string' || key === '') {
      // eslint-disable-next-line
    } else if (key.indexOf('.') !== -1) { // 支持多层级赋值操作
      const keys = key.split('.')
      inFn(obj, keys, value)
    } else {
      obj[key] = value
    }
  }
}

export const commonUtils = new CommonUtils();
